생성자와 할당자는 값을 설정하는 메커니즘
생성자(Constructor)생성자는 클래스 이름과 같은 이름을 갖는 멤버 함수
생성자는 클래스의 개체(멤버변수)를 초기화하고, 작업 환경을 만드는 메서드이다
(종종 파일, 메모리와 같은 리소스 또는 사용 후 해제해야 하는 락을 포함한다.)
class complex{
public:
complex(double rnew, double inew){
r=rnew; i=inew;
}
private:
double r, i;
}
int main(void){
complex c1(2.0, 3.0);
};
아래와 같은 멤버 변수 및 상수를 설정하는 멤버 초기화 리스트(Member Initialization List)를 지원한다.
초기화 리스트(Initialization List)
class complex{
public:
complex(double rnew, double inew): r(rnew), i(inew){}
private:
double r, i;
};
컴파일러는 디폴트로 모든 초기화하지 않은 멤버에 대해 인수없이 생성자를 호출한다.이와 같이 인자가 없는 생성자를 디폴트 생성자라고 함(Default Constructor)
class complex{
public:
complex(double rnew, double inew)
: r(), i()
{
r=rnew; i=inew;
}
};
초기화 되는 멤버가 클래스일 경우 주의하여야 함
class solver{
public:
solver(int nrows, ncols)
{
A(nrows, ncols);
}
private:
matrix_type A;
};
2번 오류처럼 코드를 작성한 경우, 생성자로 해석하지 않고, 함수 호출(A.operator()(nrows, ncols);); 로 해석한다.
따라서 클래스의 생성자를 호출하기 위해서 다음과 같이 명시해 주어야 한다.
(디폴트 생성자 사용하면 안됨)
class solver{
public:
solver(int nrows, int ncols): A(nrows, ncols) {}
private:
matrix_type A;
};
만일 solver를 호출할 때, 이미 matrix가 존재하는 경우
참조를 사용하면, 복사로 인한 메모리를 낭비하지 않고 사용할 수 있다.
class solver{
public:
solver(const matrix_type& A): A(A){}
private:
const matrix_type& A;
};
이와 같이 생성자 인수에 멤버 변수와 동일한 이름을 지정할 수 있다.
기본적으로 초기화 리스트의 이름은 스코프 밖에서 멤버를 참조하는 규칙을 가지고 있다.
인수의 이름과 지역 변수의 이름은 클래스의 이름을 숨긴다.
class complex{
public:
complex(double r, double i): r(r), i(i){}
complex(double r): r(r), i(0){}
};
class complex{
public:
complex(double r=0, double i=0): r(r), i(i){}
};
int main(void){
complex z1;
complex z2();
complex z3(4);
complex z4=4;
complex z5(0, 1);
}
위와 같이 생성자를 오버로드 할 수 있다.
만일 생성자를 생성하지 않았을 경우 default 생성자가 생성
위 코드에서 complex z2();로 정의한 z2는
디폴트 생성자 호출이 아니다.
이는 인수가 없고, complex를 반환하는 함수의 선언으로 해석된다.
인수가 없는 클래스 인스턴스를 정의하고 싶은 경우, complex z1; 으로 선언해주어야 함
특수 생성자- 디폴트 생성자(Default Constructor)
- 복사 생성자(Copy Constructor)
- 이동 생성자(Move Constructor)-C++11 이상
디폴트 생성자(Default Constructor)디폴트 생성자는 인수가 없는 생성자 혹은 모든 인수가 기본값(default value)를 갖는 생성자
디폴트 생성자가 없는 경우 리스트, 트리, 벡터, 행렬과 같은 디폴트 생성자가 없는 타입의
컨테이너를 구현하기 어려움
멤버 중 일부가 레퍼런스이거나 레퍼런스를 포함하는 경우, 디폴트 생성자를 정의하기 어려움
복사 생성자(Copy Constructor)복사 생성자를 이용해서 개체를 복사하는데 사용할 수 있다.
class complex{
public:
complex(const complex& c): i(c.i), r(c.r){};
};
int main(void){
complex z1(3.0, 2.0);
complex z2(z1);
complex z3{z1};
}
사용자가 별도로 복사 생성자를 작성하지 않은 경우, 컴파일러가 표준생성 방법으로 복사 생성자를 생성한다.
표준 생성 방법: 모든 멤버(및 베이스 클래스)의 복사 생성자를 정의한 순서대로 호출
복사 생성자를 수행해야 하는 작업의 경우(모든 멤버를 복사해야 함) 기본값(default value)를 사용해야 한다.
- 짧게 만들 수 있다.
- 오류가 적다
- 코드를 읽어볼 필요 없이, 복사 작업 이해
- 컴파일러의 최적화율 높임
복사 생성자의 인수를 const로 정의하는 것이 좋으며, 값으로 전달하면 안됨
complex(complex& c): i(c.i), r(c.r){}
complex(complex c);
값으로 인수를 전달할 경우, c에 대한 복사 생성자를 사용한다.이와 같은 작업은, 컴파일러를 무한 루프로 유도할 수 있는 자체 의존성을 만든다.
클래스에 포인터가 포함되어 있는 경우 디폴트 복사 생성자가 동작하지 않음
class vector{
public:
vector(const vector& v): my_size(v.my_size), data(new double[my_size]){
for(unsigned i=0; i<my_size; ++i) data[i]=v.data[i];
}
~vector(){delete[] data;}
private:
unsigned my_size;
double* data;
};
위의 코드에서 복사 생성자를 생략하더라도, 컴파일러는 복사 생성자를 생성하지 않는다.
만일 동작할 경우,
복사 생성자가 새로운 포인터를 생성해 주지만, 두 인스턴스내의 data 포인터는
동일한 data의 주소를 포인팅 한다.(데이터를 복사하지 않고 주소만 복사함)
이와 같은 경우, 소멸자가 instance1, instance2에 의해 두번 호출될 때,
동일한 메모리를 두 번 해제하게 된다.
위와 같은 벡터 클래스에서 데이터의 유일한 소유자임을 명시하기 위해 unique_ptr을 사용하는 것이 좋음
#include <memory>
class vector{
std::unique_ptr<double[]> data;
};
- 변환과 명시적 생성자C++은 암시적 생성자와 명시적 생성자를 구별한다.
암시적 생성자는 암시적 변환 및 할당과 같은 표기법을 사용한다.
complex c1{3.0};
complex c1(3.0);
complex c1=3.0;
위의 새 코드는 동일한 코드를 생성
암시적 변환은 필요한 타입과 다른 타입을 제공할 때 발생한다.
double inline complex_abs(complex c){
return std::sqrt(real(c)*real(c)*imag(c)*imag(c));
}
cout<<"|7|="<<complex_abs(7.0)<<'\n';
complex_abs는 double 타입을 허용하는 함수 오버로드가 존재하지 않는다.
이 때 complex 인수를 받는 오버로드가 존재하고, double 타입을 허용하는 생성자가 있기 때문에,
double 리터널에서 암시적으로 complex 값을 생성한다.
explicit 키워드를 통해 암시적 변환을 비활성화 할 수 있음
class complex{
public:
explicit complex(double nr=0.0, double i=0.0):r(nr), i(i){}
};
cout<<"|7|="<<complex_abs(complex{7.0})<<'\n';
생성자 위임(Delegating Constructor)생성자에서 다른 생성자를 호출하는 기능(C++11 부터 지원)
class complex{
public:
complex(double r, double i): r{r}, i{i} {}
complex(double r): complex{r, 0.0} {}
complex(): complex{0.0} {}
...
};
생성자 위임은 생성자 오버로딩으로 대체 가능하지만,
초기화가 더 복잡한 클래스에서 유용하게 사용 가능
멤버의 기본값C++11부터 멤버 변수의 기본값을 설정할 수 있다.
해당 기능을 통해 생성자에서 기본값과 다른 값들만 설정하면 된다.
class complex{
public:
complex(double r, double i): r{r}, i{i} {}
complex(double r): r{r} {}
complex() {}
private:
double r=0.0, i=0.0;
};
할당(Assignment)복사 생성자와 같은 방법이나, 세터와 게터를 이용하면, 사용자 클래스의 개체를 복사할 수 있다.
위와 같이 연산자를 통해서 복사하기 위해서는 할당 연산자를 제공해야 한다.
복사 할당 연산자
complex& operator=(const complex& src){
r=src.r; i=src.i
return *this;
}
멤버 r과 i를 복사한다. 연산자는 다중 할당을 할 수 있도록 개체를 가르키는 레퍼런스를 반환한다.this는 개체 자신을 가르키는 포인터이다.(*this: 레퍼런스)
위의 코드는 <complex&>=<complex &> 형태의 연산을 정의하였지만,
c=7.5 또한 암시적 변환으로 동일하게 동작한다.
complex& operator=(double nr){
r=nr; i=0;
return *this;
}
<complex&>=<double>
유의) 이전 복사와 동일하게 벡터의 복사 할당 연산자는 데이터 자체가 아닌 데이터의 주소만 복사됨
vector& operator=(const vector& src){
if(this==&src) return *this;
assert(my_size==src.my_size);
for(int i=0; i<my_size; ++i) data[i]=src.data[i];
return *this;
}
초기화 리스트(Initializer List)<initializer_list> 헤더에 포함
C++ 부터 도입된 새로운 기능이다.(초기화 리스트와 멤버의 초기화 리스트는 다름)
float v[]={1.0, 2.0, 3.0};
vector v={1.0, 2.0, 3.0};
vector v{1.0, 2.0, 3.0};
벡터를 인수로 사용하는 함수에 값을 즉시 설정
vector x=lu_solve(A, vector{1.0, 2.0, 3.0});
위와 같은 초기화 리스트를 사용하기 위해서는 vector 클래스에서 initializer_list<double>을 인수로 받는생성자와 복사 할당 연산자가 필요하다.
#include <initializer_list>
#include <algorithm>
class vector{
vector(std::initializer_list<double> values)
:my_size(values.size()), data(new double[my_size]){
std::copy(std::begin(values), std::end(values), std::begin(data));
}
vector& operator=(std::initializer_list<double> values){
assert(my_size==values.size());
std::copy(std::begin(values), std::end(values), std::begin(data));
return *this;
}
};
std::copy(입력의 시작, 입력의 끝, 출력의 시작)
initializer_list<double>형의 리스트를 data에 저장
유니폼 초기화(Uniform initialization)중괄호 { }는 모든 형태의 변수 초기화에 대한 보편적인 표기법으로, C++11에서 사용한다.
- 초기화 리스트 생성자
- 다른 생성자
- 직접 멤버 설정
complex c{7,0, 8}, c2={0, 1}, c3={9.3}, c4={c};
const complex cc={c3};
유니폼 초기화의 인수로 초기화 리스트를 사용하면, 원래는 두 개의 중괄호를 사용해야 한다.
하지만, C++11은 중괄호 생략(Brace Elision)을 제공한다.
vector v1={{1.0, 2.0, 3.0}};
vector v2={1.0, 2.0, 3.0};
만일 vector class와 complex class를 결합한 vector_complex class를 정의했다고 하자
vector_complex v={{1.5, -2}, {3.4}, {2.6, 5.13}};
vector_complex v1d={{2}};
vector complex v2d={{2, 3}};
vector complex v3d{{2, 3, 4}};
vector에는 3개의 인수로 호출되는 생성자가 존재하지 않기 떄문에,
각각 하나의 인자로 구성된 여러 벡터 항목으로 전환한다.
멤버 변수의 초기화에도 중괄호를 사용할 수 있다.
class vector{
public:
vector(unsigned n) :my_size{n}, data{new double[my_size]}{}
...
private:
unsigned my_size;
double* data;
};
초기화 리스트를 이용하면, 기본 타입이 아닌 함수의 인수를 즉석에서 작성할 수 있다.
double d=dot(vector{3, 4, 5}, vecot{7, 8, 9});
즉석으로 vector(3, 4, 5)와 vecot(7, 8, 9)를 생성해서 인자로 전달
인수의 타입의 명확한 경우(하나의 오버로드만 사용 가능한 경우)
리수트는 함수에 타입을 명시하지 않고도 전달할 수 있다.
double d=dot({3, 4, 5}, {7, 8, 9});
함수의 결과를 중괄호 표기법으로 설정할 수 있다.
complex substract(const complex& c1, const complex& c2){
return {c1.r-c2.r, c1.i-c2.i};
}
함수의 리턴타입은 complex이며, 두 개의 인수로 된 중괄호 목록으로 초기화한 후, 반환한다.
이동 문법많은 양의 데이터를 복사하는 동작은 비용이 많이 들기 때문에 여러 소프트웨어에서는 주소만 복사하는 등
얕은 복사를 사용한다.
v[7]을 변경하면, w[7]도 변화게 된다.
따라서 얕은 복사를 수행하는 소프트웨어는 일반적으로 깊은 복사를 호출하는 함수를 제공한다.
이동 생성자(&&)변수는(모든 명명된 항목)은 깊은 복사를 하고,
임시 값(이름으로 참조할 수 없는 개체)이 데이터를 전송
이동 생성자와 이동 할당 연산자를 구현하면, 값비싼 비용으로 복사하지 않아도 된다.
class vector{
vector(vector&& v): my_size(v.my_size), data(v.data){
v.data=0;
v.my_size=0;
}
};
이동 생성자는 원본에서 데이터를 훔쳐 빈 상태로 만든다.Rvalue로 전달된 개체는 함수로 반환한 뒤, 만료되었다고 간주한다.
(개체의 파괴가 실패하지 않아야 한다.)
이동 할당 연산자
class vector{
vector& operator=(vector&& src){
assert(my_size==0||my_size==src.my_size);
std::swap(data, src.data);
return *this;
}
};
복사 생략(Copy Elision)
최신의 컴파일러는 이동 생성자를 잘 사용하지 않는다.
더 나은 최적화를 제공하는 복사 생략(Copy Elision)을 사용한다.
컴파일러는 복사본을 생략하고, 복사 작업의 대상 주소에 즉시 저장하도록 데이터 생성 코드를 수정한다.
inline vector ones(int n){
vector v(n);
for(unsigned i=0; i<n; ++i) v[i]=1.0;
return v;
}
...
vector w(ones(7));
컴파일러는 v를 생성하고 함수 끝에서 w로 복사(또는 이동)하는 대신 w를 즉시 생성하고,
모든 연산을 w에서 바로 수행한다.
이동 문법이 필요한 곳std::move와 같은 함수를 사용하는 경우 이동 문법이 반드시 필요하다.
std::move 함수는 이동하는 동작을 수행하는 것이 아닌 Lvalue를 Rvalue로 변환한다.
변수가 임시변수인 척해서, 이동 가능하게 한다.
vector x(std::move(w));
v=std::move(u);
x는 w의 데이터를 훔쳐 빈 벡터로 만든다.
v와 u는 데이터를 교환한다.
class vector{
vector& operator=(vector&& src){
assert(my_size==src.my_size);
delete[] data;
data=src.data;
src.data=nullptr;
src.my_size=0;
return *this;
}
};